/*
 * Copyright (C) 2022 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "avro_napi.h"

napi_value AvroTypeC::FromBuffer(napi_env env, napi_callback_info info) {
    size_t argc = 1;
    napi_value args[1];
    napi_value jsthis;
    napi_status status = napi_get_cb_info(env, info, &argc, args, &jsthis, nullptr);
    if (status != napi_ok || argc < 1) {
        LOGERROR("Expected 1 argument (FromBuffer)");
        return nullptr;
    }

    if (!NapiUtil::IsArrayBuffer(env, args[0])) {
        LOGERROR("First argument must be ArrayBuffer");
        return nullptr;
    }

    void *buffer_data;
    size_t buffer_len;
    status = napi_get_arraybuffer_info(env, args[0], &buffer_data, &buffer_len);
    if (status != napi_ok) {
        LOGERROR("Get buffer info failed！");
        return nullptr;
    }
    if (buffer_len == 0) {
        LOGERROR("Empty ArrayBuffer");
        return nullptr;
    }

    avro_reader_t reader = avro_reader_memory((char *)buffer_data, buffer_len);
    if (!reader) {
        LOGERROR("Failed to create memory reader");
        return nullptr;
    }
    AvroTypeC *obj;
    status = napi_unwrap(env, jsthis, (void **)&obj);
    if (status != napi_ok) {
        LOGERROR("napi_unwrap failed！");
        avro_reader_free(reader);
        return nullptr;
    }
    avro_value_t value;
    napi_value result;
    avro_value_iface_t *iface;
    iface = avro_generic_class_from_schema(obj->schema_);
    if (iface == nullptr) {
        LOGINFO("[%{public}s] avro_generic_class_from_schema return null! ", __func__);
        avro_reader_free(reader);
        return nullptr;
    }

    int ret = avro_generic_value_new(iface, &value);
    if (ret != 0) {
        LOGERROR("Failed to create value");
        avro_reader_free(reader);
        avro_value_iface_decref(iface);
        return nullptr;
    }

    ret = avro_value_read(reader, &value);
    if (ret != 0) {
        LOGERROR("Failed to avro_value_read");
        avro_reader_free(reader);
        avro_value_iface_decref(iface);
        avro_value_decref(&value);
        return nullptr;
    }

    result = AvroValueToNapi(env, obj->schema_, value);

    avro_reader_free(reader);
    avro_value_iface_decref(iface);
    avro_value_decref(&value);

    return result;
}


napi_value AvroTypeC::FromBuffer_string(napi_env env, avro_value_t &value) {
    const char *str;
    size_t len;
    int ret = avro_value_get_string(&value, &str, &len);
    if (ret != 0) {
        LOGERROR("Failed to avro_value_get_string");
        return nullptr;
    }

    napi_value result;
    napi_status status = napi_create_string_utf8(env, str, NAPI_AUTO_LENGTH, &result);
    if (status != napi_ok) {
        LOGERROR("Failed to create GetFixedSize result!");
        return nullptr;
    }

    return result;
}

napi_value AvroTypeC::FromBuffer_bytes(napi_env env, avro_value_t &value) {
    const void *bytes;
    size_t len;
    int ret = avro_value_get_bytes(&value, &bytes, &len);
    if (ret != 0) {
        LOGERROR("Failed to avro_value_get_bytes");
        return nullptr;
    }
    napi_value arraybuffer;
    void *arraybuffer_data;
    napi_status status = napi_create_arraybuffer(env, len, &arraybuffer_data, &arraybuffer);
    if (status != napi_ok) {
        LOGERROR("Failed to napi_create_arraybuffer result!");
        return nullptr;
    }
    memcpy(arraybuffer_data, bytes, len);

    napi_value uint8array;
    status = napi_create_typedarray(env, napi_uint8_array, len, arraybuffer, 0, &uint8array);
    if (status != napi_ok) {
        LOGERROR("Failed to napi_create_typedarray result!");
        return nullptr;
    }

    return uint8array;
}

napi_value AvroTypeC::FromBuffer_int32(napi_env env, avro_value_t &value) {
    int resint = 0;
    int ret = avro_value_get_int(&value, &resint);
    if (ret != 0) {
        LOGERROR("Failed to avro_value_get_int");
        return nullptr;
    }

    napi_value result;
    napi_status status = napi_create_int32(env, resint, &result);
    if (status != napi_ok) {
        LOGERROR("Failed to create GetFixedSize result!");
        return nullptr;
    }

    return result;
}

napi_value AvroTypeC::FromBuffer_int64(napi_env env, avro_value_t &value) {
    long res = 0;
    int ret = avro_value_get_long(&value, &res);
    if (ret != 0) {
        LOGERROR("Failed to avro_value_get_int");
        return nullptr;
    }

    napi_value result;
    napi_status status = napi_create_int64(env, res, &result);
    if (status != napi_ok) {
        LOGERROR("Failed to create GetFixedSize result!");
        return nullptr;
    }

    return result;
}

napi_value AvroTypeC::FromBuffer_float(napi_env env, avro_value_t &value) {
    float res = 0;
    int ret = avro_value_get_float(&value, &res);
    if (ret != 0) {
        LOGERROR("Failed to avro_value_get_int");
        return nullptr;
    }

    napi_value result;
    napi_status status = napi_create_double(env, static_cast<double>(res), &result);
    if (status != napi_ok) {
        LOGERROR("Failed to create GetFixedSize result!");
        return nullptr;
    }

    return result;
}

napi_value AvroTypeC::FromBuffer_double(napi_env env, avro_value_t &value) {
    double res = 0;
    int ret = avro_value_get_double(&value, &res);
    if (ret != 0) {
        LOGERROR("Failed to avro_value_get_int");
        return nullptr;
    }

    napi_value result;
    napi_status status = napi_create_double(env, static_cast<double>(res), &result);
    if (status != napi_ok) {
        LOGERROR("Failed to create GetFixedSize result!");
        return nullptr;
    }

    return result;
}

napi_value AvroTypeC::FromBuffer_boolean(napi_env env, avro_value_t &value) {
    int res;
    int ret = avro_value_get_boolean(&value, &res);
    if (ret != 0) {
        LOGERROR("Failed to FromBuffer_boolean");
        return nullptr;
    }

    napi_value result;
    napi_status status = napi_get_boolean(env, res != 0, &result);
    if (status != napi_ok) {
        LOGERROR("Failed to create GetFixedSize result!");
        return nullptr;
    }

    return result;
}

napi_value AvroTypeC::FromBuffer_null(napi_env env, avro_value_t &value) {
    napi_value null_val;
    if (napi_get_null((env), &null_val) != napi_ok) {
        LOGERROR("Failed to create null value!");
        return nullptr;
    }
    return null_val;
}

napi_value AvroTypeC::FromBuffer_record(napi_env env, avro_schema_t schema, avro_value_t &value) {
    napi_value result;
    napi_status status = napi_create_object(env, &result);
    if (status != napi_ok) {
        LOGERROR("Failed to create napi_create_object result!");
        return nullptr;
    }

    size_t filedCount = avro_schema_record_size(schema);

    for (size_t i = 0; i < filedCount; i++) {
        const char *key;
        avro_value_t filedValue;

        if (avro_value_get_by_index(&value, i, &filedValue, &key) != 0) {
            fprintf(stderr, "Error: Failed to get key-value pair at index %zu\n", i);
            continue;
        }

        napi_value js_key;
        status = napi_create_string_utf8(env, key, NAPI_AUTO_LENGTH, &js_key);
        if (status != napi_ok) {
            LOGERROR("Create string failed!");
            return nullptr;
        }

        avro_schema_t valSchema = avro_schema_record_field_get(schema, key);
        napi_value js_val = AvroValueToNapi(env, valSchema, filedValue);

        status = napi_set_property(env, result, js_key, js_val);
        if (status != napi_ok) {
            continue;
        }
    }

    return result;
}

napi_value AvroTypeC::FromBuffer_enum(napi_env env, avro_schema_t schema, avro_value_t &value) {
    napi_value result;

    int index;
    int ret = avro_value_get_enum(&value, &index);
    if (ret != napi_ok) {
        LOGERROR("Failed to get ENUM symbol");
        return nullptr;
    }

    int num_symbols = avro_schema_enum_number_of_symbols(schema);
    if (index > num_symbols - 1) {
        LOGERROR("Failed to create GetFixedSize result!");
        return nullptr;
    }
    const char *enmuStr = avro_schema_enum_get(schema, index);
    napi_status status = napi_create_string_utf8(env, enmuStr, NAPI_AUTO_LENGTH, &result);
    if (status != napi_ok) {
        LOGERROR("Failed to napi_create_string_utf8 result!");
        return nullptr;
    }

    return result;
}

napi_value AvroTypeC::FromBuffer_fixed(napi_env env, avro_value_t &value) {
    const void *data;
    size_t size;
    int ret = avro_value_get_fixed(&value, &data, &size);
    if (ret != 0) {
        LOGERROR("Failed to avro_value_get_fixed");
        return nullptr;
    }

    napi_value js_obj;
    napi_status status = napi_create_array(env, &js_obj);
    if (status != napi_ok) {
        LOGERROR("创建JavaScript对象失败");
        return nullptr;
    }

    for (int32_t i = 0; i < size; ++i) {
        napi_value result;
        char *aa = (char *)data;
        napi_status status = napi_create_int32(env, aa[i], &result);
        if (status != napi_ok) {
            LOGERROR("Create int32 failed!");
            return nullptr;
        }
        napi_set_element(env, js_obj, (uint32_t)i, result);
    }

    return js_obj;
}

napi_value AvroTypeC::FromBuffer_map(napi_env env, avro_schema_t schema, avro_value_t &map_value) {
    size_t map_size = 0;
    if (avro_value_get_size(&map_value, &map_size) != 0) {
        LOGERROR("Failed to get map size");
        return nullptr;
    }

    napi_value js_obj;
    napi_status status = napi_create_object(env, &js_obj);
    if (status != napi_ok) {
        LOGERROR("创建JavaScript对象失败");
        return nullptr;
    }

    napi_value js_val;
    for (size_t i = 0; i < map_size; i++) {
        const char *key;
        avro_value_t value;

        if (avro_value_get_by_index(&map_value, i, &value, &key) != 0) {
            fprintf(stderr, "Error: Failed to get key-value pair at index %zu\n", i);
            continue;
        }

        avro_schema_t valSchema = avro_schema_map_values(schema);
        js_val = AvroValueToNapi(env, valSchema, value);

        status = napi_set_named_property(env, js_obj, key, js_val);
        if (status != napi_ok) {
            LOGERROR("设置对象属性失败");
            return nullptr;
        }
    }

    return js_obj;
}

napi_value AvroTypeC::AvroValueToNapi(napi_env env, avro_schema_t schema, avro_value_t &value) {
    napi_value result;

    int type = avro_value_get_type(&value);
    switch (type) {
    case AVRO_STRING: {
        return FromBuffer_string(env, value);
    }
    case AVRO_BYTES: {
        return FromBuffer_bytes(env, value);
    }
    case AVRO_INT32: {
        return FromBuffer_int32(env, value);
    }
    case AVRO_INT64: {
        return FromBuffer_int64(env, value);
    }
    case AVRO_FLOAT: {
        return FromBuffer_float(env, value);
    }
    case AVRO_DOUBLE: {
        return FromBuffer_double(env, value);
    }
    case AVRO_BOOLEAN: {
        return FromBuffer_boolean(env, value);
    }
    case AVRO_NULL: {
        napi_get_null(env, &result);
        return result;
    }
    case AVRO_RECORD: {
        return FromBuffer_record(env, schema, value);
    }
    case AVRO_ENUM: {
        return FromBuffer_enum(env, schema, value);
    }
    case AVRO_FIXED: {
        return FromBuffer_fixed(env, value);
    }
    case AVRO_MAP: {
        return FromBuffer_map(env, schema, value);
    }
    case AVRO_ARRAY: {
        return FromBuffer_array(env, schema, value);
    }
    case AVRO_UNION: {
        return FromBuffer_union(env, schema, value);
    }
    default:
        LOGERROR("unknown type:%{public}d", type);
        break;
    }

    return nullptr;
}

napi_value AvroTypeC::FromBuffer_array(napi_env env, avro_schema_t schema, avro_value_t &value) {
    napi_value js_obj;
    napi_status status = napi_create_array(env, &js_obj);
    if (status != napi_ok) {
        LOGERROR("创建JavaScript对象失败");
        return nullptr;
    }

    size_t elementCount = 0;
    int ret = avro_value_get_size(&value, &elementCount);
    if (ret != napi_ok) {
        LOGERROR("Failed to avro_value_get_size");
        return nullptr;
    }
    avro_schema_t valSchema = avro_schema_array_items(schema);
    for (int32_t i = 0; i < elementCount; ++i) {
        avro_value_t element;
        if (avro_value_get_by_index(&value, i, &element, nullptr) != 0) {
            LOGERROR("Failed to avro_value_get_by_index");
            continue;
        }

        napi_value js_val = AvroValueToNapi(env, valSchema, element);

        napi_set_element(env, js_obj, (uint32_t)i, js_val);
    }
    return js_obj;
}

napi_value AvroTypeC::FromBuffer_union(napi_env env, avro_schema_t schema, avro_value_t &value) {
    napi_value result;
    int branch = 0;

    if (avro_value_get_discriminant(&value, &branch) != 0) {
        LOGERROR("Failed to get union discriminant");
        return NULL;
    }
    avro_schema_t valSchema = avro_schema_union_branch(schema, branch);

    avro_value_t branch_value;
    if (avro_value_get_current_branch(&value, &branch_value) != 0) {
        LOGERROR("Failed to get union branch value");
        return nullptr;
    }

    result = AvroValueToNapi(env, valSchema, branch_value);

    return result;
}

void FreeAvroAsyncData(napi_env env, AvroAsyncData *data) {
    if (data == nullptr) {
        return;
    }

    if (data->callback != nullptr) {
        napi_delete_reference(env, data->callback);
        data->callback = nullptr;
    }

    if (!data->result.empty()) {
        for (auto res : data->result) {
            avro_value_decref(&res);
        }
    }

    if (data->schema != nullptr) {
        avro_schema_decref(data->schema);
        data->schema = nullptr;
    }
    if (data->reader != nullptr) {
        avro_file_reader_close(data->reader);
        data->reader = nullptr;
    }
    if (data->writer != nullptr) {
        avro_file_writer_close(data->writer);
        data->writer = nullptr;
    }
    if (!data->write_values.empty()) {
        for (auto res : data->write_values) {
            avro_value_decref(&res);
        }
    }

    free(data);
    data = nullptr;
}

void ReadAvroWorker(napi_env env, void *data) {
    AvroAsyncData *asyncData = static_cast<AvroAsyncData *>(data);
    if (asyncData == nullptr) {
        LOGERROR("Param is error!");
        return;
    }
    int res = avro_file_reader(asyncData->filePath.c_str(), &asyncData->reader);
    if (res != napi_ok || asyncData->reader == nullptr) {
        asyncData->errorMsg = "Failed to avro_file_reader,res:" + std::to_string(res);
        LOGERROR("Failed to avro_file_reader,res:%{public}d", res);
        return;
    }
    napi_value callbackArgs[2];
    napi_status status = napi_get_null(env, &callbackArgs[0]);
    STATUS_CHECK(status);

    avro_value_iface_t *iface = avro_generic_class_from_schema(asyncData->schema);
    if (!iface) {
        asyncData->errorMsg = "Failed to create generic class from schema";
        LOGERROR("Failed to create generic class from schema");
        avro_file_reader_close(asyncData->reader);
        asyncData->reader = nullptr;
        return;
    }

    avro_value_t recordVal;
    int ret = avro_generic_value_new(iface, &recordVal);
    if (ret != 0) {
        asyncData->errorMsg = "Failed to create value";
        LOGERROR("Failed to create value");
        avro_file_reader_close(asyncData->reader);
        asyncData->reader = nullptr;
        avro_value_iface_decref(iface);
        return;
    }

    while (true) {
        int readRet = avro_file_reader_read_value(asyncData->reader, &recordVal);
        if (readRet != 0) {
            if (readRet != EOF) {
                asyncData->errorMsg = "Failed to read Avro record,ret=" + std::to_string(readRet);
                LOGERROR("Failed to read Avro record,ret=%{public}d", ret);
            }
            break;
        } else {
            avro_value_t dest;
            int ret = avro_generic_value_new(iface, &dest);
            ret = avro_value_copy(&dest, &recordVal);
            if (ret != 0) {
                asyncData->errorMsg = "Failed to copy avro value!";
                LOGERROR("%{public}s,ret=%{public}d", asyncData->errorMsg.c_str(), ret);
                break;
            }
            asyncData->result.push_back(dest);
        }
        avro_value_reset(&recordVal);
    }
    avro_value_decref(&recordVal);
    avro_file_reader_close(asyncData->reader);
    asyncData->reader = nullptr;
    avro_value_iface_decref(iface);
}

void ReadAvroComplete(napi_env env, napi_status status, void *data) {
    AvroAsyncData *asyncData = static_cast<AvroAsyncData *>(data);
    if (asyncData == nullptr || asyncData->callback == nullptr) {
        FreeAvroAsyncData(env, asyncData);
        LOGERROR("Param is error!");
        return;
    }

    napi_value callbackArgs[2];
    if (!asyncData->errorMsg.empty()) {
        status =
            napi_create_string_utf8(env, asyncData->errorMsg.c_str(), asyncData->errorMsg.length(), &callbackArgs[0]);
        STATUS_CHECK(status);
        status = napi_get_null(env, &callbackArgs[1]);
        STATUS_CHECK(status);
    } else {
        status = napi_get_null(env, &callbackArgs[0]);
        STATUS_CHECK(status);
        if (asyncData->result.size() > 1) {
            status = napi_create_array(env, &callbackArgs[1]);
            STATUS_CHECK(status);

            for (int i = 0; i < asyncData->result.size(); i++) {
                napi_value jsRecord = AvroTypeC::AvroValueToNapi(env, asyncData->schema, asyncData->result[i]);
                status = napi_set_element(env, callbackArgs[1], i, jsRecord);
                STATUS_CHECK(status);
            }
        } else {
            callbackArgs[1] = AvroTypeC::AvroValueToNapi(env, asyncData->schema, asyncData->result[0]);
        }
    }

    napi_value callback;
    status = napi_get_reference_value(env, asyncData->callback, &callback);
    STATUS_CHECK(status);

    napi_value global;
    status = napi_get_global(env, &global);
    STATUS_CHECK(status);
    napi_value result2;
    status = napi_call_function(env, global, callback, 2, callbackArgs, &result2);
    STATUS_CHECK(status);

    FreeAvroAsyncData(env, asyncData);
}

napi_value FromFile(napi_env env, napi_callback_info info) {
    size_t argc = 3;
    napi_value args[3];

    if (napi_get_cb_info(env, info, &argc, args, nullptr, nullptr) != napi_ok || argc < 3) {
        LOGERROR("INVALID_ARG:  Need 2 arguments: filePath (string), callback (function)");
        napi_throw_error(env, NULL, "Need 2 arguments: filePath (string), callback (function)");
        return nullptr;
    }

    napi_valuetype path_type;
    napi_valuetype schema_type;
    napi_valuetype cb_type;
    napi_status status = napi_typeof(env, args[0], &path_type);
    if (status != napi_ok) {
        napi_throw_error(env, NULL, "napi_typeof error!");
        return nullptr;
    }
    status = napi_typeof(env, args[1], &schema_type);
    if (status != napi_ok) {
        napi_throw_error(env, NULL, "napi_typeof error!");
        return nullptr;
    }
    status = napi_typeof(env, args[2], &cb_type);
    if (status != napi_ok) {
        napi_throw_error(env, NULL, "napi_typeof error!");
        return nullptr;
    }
    if (path_type != napi_string || schema_type != napi_string || cb_type != napi_function) {
        LOGERROR("Type error: filePath/schemaStr = string, callback = function");
        napi_throw_error(env, NULL, "Type error: filePath/schemaStr = string, callback = function");
        return nullptr;
    }

    AvroAsyncData *asyncData = static_cast<AvroAsyncData *>(calloc(1, sizeof(AvroAsyncData)));
    if (asyncData == nullptr) {
        LOGERROR("MEMORY_ERR:Failed to allocate async data");
        napi_throw_error(env, NULL, "MEMORY_ERR:Failed to allocate async data");
        return nullptr;
    }

    size_t path_len;
    status = napi_get_value_string_utf8(env, args[0], NULL, 0, &path_len);
    if (status != napi_ok) {
        FreeAvroAsyncData(env, asyncData);
        std::string errMsg = "napi_typeof Fail, status:" + std::to_string(status);
        napi_throw_error(env, NULL, errMsg.c_str());
        return nullptr;
    }
    char *buffer = new char[path_len + 1];
    size_t copied;
    status = napi_get_value_string_utf8(env, args[0], buffer, path_len + 1, &copied);
    if (status != napi_ok) {
        FreeAvroAsyncData(env, asyncData);
        std::string errMsg = "napi_typeof Fail, status:" + std::to_string(status);
        napi_throw_error(env, NULL, errMsg.c_str());
        return nullptr;
    }
    buffer[copied] = '\0';
    asyncData->filePath = buffer;
    asyncData->filePath.length();
    delete[] buffer;
    buffer = nullptr;

    std::string json_str = AvroTypeC::GetStringFromNapiValue(env, args[1]);
    if (json_str.empty()) {
        FreeAvroAsyncData(env, asyncData);
        LOGERROR("Get avro scheam string failed!");
        napi_throw_error(env, NULL, "Get avro scheam string failed!");
        return nullptr;
    }
    avro_schema_error_t error;
    int ret = avro_schema_from_json(json_str.c_str(), (int32_t)json_str.length(), &asyncData->schema, &error);
    if (ret != 0) {
        LOGERROR("avro parse scheam failed, ret=%{public}d", ret);
        FreeAvroAsyncData(env, asyncData);
        std::string errMsg = "navro parse scheam failed, status:" + std::to_string(ret);
        napi_throw_error(env, NULL, errMsg.c_str());
        return nullptr;
    }

    status = napi_create_reference(env, args[2], 1, &asyncData->callback);
    if (status != napi_ok) {
        FreeAvroAsyncData(env, asyncData);
        std::string errMsg = "napi_create_reference failed, status:" + std::to_string(status);
        napi_throw_error(env, NULL, errMsg.c_str());
        return nullptr;
    }

    napi_value resourceName;
    status = napi_create_string_utf8(env, "ReadAvroWorker", NAPI_AUTO_LENGTH, &resourceName);
    if (status != napi_ok) {
        FreeAvroAsyncData(env, asyncData);
        std::string errMsg = "napi_create_string_utf8 failed, status:" + std::to_string(status);
        napi_throw_error(env, NULL, errMsg.c_str());
        return nullptr;
    }
    if (napi_create_async_work(env, nullptr, resourceName, ReadAvroWorker, ReadAvroComplete, asyncData,
                               &asyncData->work) != napi_ok) {
        LOGERROR("ASYNC_ERR: Failed to create async work");
        FreeAvroAsyncData(env, asyncData);
        napi_throw_error(env, NULL, "ASYNC_ERR: Failed to create async work");
        return nullptr;
    }

    status = napi_queue_async_work(env, asyncData->work);
    if (status != napi_ok) {
        FreeAvroAsyncData(env, asyncData);
        std::string errMsg = "napi_queue_async_work failed, status:" + std::to_string(status);
        napi_throw_error(env, NULL, errMsg.c_str());
        return nullptr;
    }

    napi_value undefined;
    status = napi_get_undefined(env, &undefined);
    return undefined;
}
